﻿namespace Sai.Core
{
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.IO;
	using System.Linq;
	using System.Xml.Serialization;
	using Sai.Core.Introspection;
    using Sai.Core.Reporting;

	[XmlRoot("AssemblyDifference")]
    public class AssemblyDifference : IAssemblyDifference
	{
		private Identifier ident = new Identifier();

		private List<TypeName> added = new List<TypeName>();

		private List<ObsoleteTypeDifference> obsolete = new List<ObsoleteTypeDifference>();

		private List<ITypeDifference> changeDetails = new List<ITypeDifference>();

		private List<TypeName> removed = new List<TypeName>();

		private string summaryIdent;

		private string summaryName;

		private string assemblyName;

		/// <summary>
		/// Identifier used to link back to the summary page.
		/// </summary>
		[XmlAttribute("SummaryID")]
		public string SummaryIdentifier 
		{ 
			get
			{
				return this.summaryIdent;
			}
			
			set
			{
				this.summaryIdent = value;

				foreach (var detail in this.changeDetails)
				{
					detail.SummaryIdentifier = value;
				}
			}
		}

		/// <summary>
		/// Name used to link back to the summary page.
		/// </summary>
		[XmlAttribute("SummaryName")]
		public string SummaryName
		{
			get
			{
				return this.summaryName;
			}

			set
			{
				this.summaryName = value;
				
				foreach (var detail in this.changeDetails)
				{
					detail.SummaryName = value;
				}
			}
		}

		/// <summary>
		/// Identifier for this assembly.
		/// </summary>
		[XmlAttribute("ID")]
		public string Identifier
		{
			get
			{
				return this.ident.Value;
			}

			set
			{
				this.ident = new Identifier(value);

				foreach (var detail in this.changeDetails)
				{
					detail.AssemblyIdentifier = value;
				}
			}
		}

		/// <summary>
		/// The name of the assembly.
		/// </summary>
		[XmlAttribute("Name")]
		public string Name
		{
			get
			{
				return this.assemblyName;
			}

			set
			{
				this.assemblyName = value;

				foreach (var detail in this.changeDetails)
				{
					detail.AssemblyName = value;
				}
			}
		}

		/// <summary>
		/// The old version of the assembly.
		/// </summary>
		[XmlAttribute("From")]
		public string FromVersion
		{
			get;
			set;
		}

		/// <summary>
		/// The new version of the assembly.
		/// </summary>
		[XmlAttribute("To")]
		public string ToVersion
		{
			get;
			set;
		}

		/// <summary>
		/// The name of our application (used to generate footer documentation).
		/// </summary>
		[XmlAttribute("ProductName")]
		public string ProductName
		{
			get;
			set;
		}

		/// <summary>
		/// The link to the app's web page (used to generate footer documentation).
		/// </summary>
		[XmlAttribute("ProductLink")]
		public string ProductLink
		{
			get;
			set;
		}

		/// <summary>
		/// The current version of the app (used to generate footer documentation).
		/// </summary>
		[XmlAttribute("ProductVersion")]
		public string ProductVersion
		{
			get;
			set;
		}


        [XmlAttribute("PercentChurn")]
        public int PercentChurn
        {
            get;
            set;
        }

		/// <summary>
		/// Types added to the assembly.
		/// </summary>
        [XmlArray("AddedTypes", IsNullable = false)]
		[XmlArrayItem("Type", typeof(TypeName))]
        public Collection<TypeName> AddedTypes
		{
			get
			{
                if (this.added.Any())
                {
                    return new Collection<TypeName>(this.added);
                }

                return null;
			}
		}

		/// <summary>
		/// Types present but marked as obsolete in the assembly.
		/// </summary>
        [XmlArray("ObsoleteTypes", IsNullable = false)]
		[XmlArrayItem("Type", typeof(ObsoleteTypeDifference))]
        public Collection<ObsoleteTypeDifference> ObsoleteTypes
		{
			get
			{
                if (this.obsolete.Any())
                {
                    return new Collection<ObsoleteTypeDifference>(this.obsolete);
                }

                return null;
			}
		}

		/// <summary>
		/// Types changed for whatever reason in the assembly (used to generate assembly-level links).
		/// </summary>
        [XmlArray("ChangedTypes", IsNullable = false)]
		[XmlArrayItem("TypeNameReference", typeof(TypeNameReference))]
        public Collection<TypeNameReference> ChangedTypes
		{
			get
			{
                if (this.changeDetails.Any())
                {
                    List<TypeNameReference> typeReferences = new List<TypeNameReference>();

                    foreach (ITypeDifference diff in this.changeDetails)
                    {
                        typeReferences.Add(new TypeNameReference(diff.Name, diff.NamespaceName, diff.Identifier));
                    }

                    return new Collection<TypeNameReference>(typeReferences);
                }

                return null;
			}
		}

		/// <summary>
		/// Full details of changes to each type.
		/// </summary>
		[XmlIgnore]
		public Collection<ITypeDifference> ChangeDetails 
		{ 
			get
			{
				return new Collection<ITypeDifference>(this.changeDetails);
			}
		}
		
		/// <summary>
		/// Types removed from the assembly.
		/// </summary>
		[XmlArray("RemovedTypes", IsNullable = false)]
		[XmlArrayItem("Type", typeof(TypeName))]
		public Collection<TypeName> RemovedTypes
		{
			get
			{
                if (this.removed.Any())
                {
                    return new Collection<TypeName>(this.removed);
                }

                return null;
			}
		}

		/// <summary>
		/// Have any changes been identified in this assembly.
		/// </summary>
		[XmlIgnore]
		public bool HasAnyChanges
		{
			get
			{
				return this.added.Any() 
                    || this.removed.Any()
                    || this.changeDetails.Any()
                    || this.obsolete.Any();
			}
		}

		/// <summary>
		/// A type was added.
		/// </summary>
		/// <param name="typeName"></param>
		public void Added(TypeName typeName)
		{
			this.added.Add(typeName);
		}

		/// <summary>
		/// A type was marked obsolete.
		/// </summary>
		/// <param name="typeName"></param>
		/// <param name="message"></param>
		public void Obsolete(TypeName typeName, string message)
		{
			this.obsolete.Add(new ObsoleteTypeDifference(typeName, message));
		}

		/// <summary>
		/// A type was changed.
		/// </summary>
		/// <param name="difference"></param>
		public void Changed(ITypeDifference difference)
		{
			this.changeDetails.Add(difference);
		}

		/// <summary>
		/// A type was removed.
		/// </summary>
		/// <param name="typeName"></param>
		public void Removed(TypeName typeName)
		{
			this.removed.Add(typeName);
		}

		/// <summary>
		/// Save results to a file.
		/// </summary>
		/// <param name="fileName"></param>
		public void SaveTo(string fileName)
		{
			using (var writer = new StreamWriter(fileName))
			{
				this.SaveTo(writer);
			}
		}

		/// <summary>
		/// Save to a stream.
		/// </summary>
		/// <param name="writer"></param>
		public void SaveTo(TextWriter writer)
		{
            ObjectFlattener<AssemblyDifference>.Flatten(this, writer);
		}
		
		/// <summary>
		/// Convert to xml serialized form.
		/// </summary>
		/// <returns>An xml formatted string.</returns>
		public string ToXml()
		{
			using (var writer = new StringWriter())
			{
				this.SaveTo(writer);
				return writer.ToString();
			} 
		}

		/// <summary>
		/// Short-hand for basic assembly info.
		/// </summary>
		/// <returns>A string describing the assembly comparison.</returns>
		public override string ToString()
		{
			if (string.IsNullOrEmpty(this.Name))
			{
				return "<unknown>";
			}

			return string.Format("{0} {1} {2}", this.Name, this.FromVersion, this.ToVersion);
		}

		/// <summary>
		/// Short-hand for basic assembly info.
		/// </summary>
		/// <returns>A positive integer.</returns>
		public override int GetHashCode()
		{
			if (string.IsNullOrEmpty(this.Name))
			{
				return 0;
			}

			return this.Name.GetHashCode();
		}
	}
}
